﻿/*
   Copyright 2012 Dmitry Bratus

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
*/
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Security.AccessControl;
using System.Text;
using System.Threading.Tasks;
using Flower.Directory.Util;
using System.IO;
using System.Runtime.Serialization;
using Flower.Logging;
using System.Xml;
using System.Transactions;
using Flower.Services;
using Flower.Services.Data;
using Ionic.Zip;

namespace Flower.Directory.Management
{
    public static class DirectoryImporterExporter
    {
        private static readonly Log Log = LogManager.CreateLog(typeof(DirectoryImporterExporter).Name);

        #region Import
        
        public static IDictionary<string, string> ImportFromFileSystem(this IDirectory dir, string path)
        {
            Log.Trace(() => string.Format("Importing from file system directory {0}.", path));

            return Import(new FileSystemDirectoryImportSource(), dir, path);
        }

        public static IDictionary<string, string> ImportFromArchive(this IDirectory dir, string path)
        {
            Log.Trace(() => string.Format("Importing from archive {0}.", path));

            using (var stream = new FileStream(path, FileMode.Open, FileAccess.Read))
            {
                return dir.ImportFromArchive(stream);
            }
        }

        public static IDictionary<string, string> ImportFromArchive(this IDirectory dir, Stream zipStream)
        {
            Log.Trace("Importing from archive stream");

            using (var zipFile = ZipFile.Read(zipStream))
            {
                return Import(new ZipFileDirectoryImportSource(zipFile), dir, string.Empty);
            }
        }

        private struct LinkInfo
        {
            public string Parent;
            public Link Link;
        }

        #region DirectoryEntryReader

        private class DirectoryEntryReader : IDirectoryEntryVisitor
        {
            public IDirectoryImportSource Source;
            public string Path;

            private XmlDocument LoadDirectoryEntryData(DirectoryEntry entry)
            {
                string dataFileName = string.Concat(Path, "\\", entry.Name, ".data.xml");

                if (Source.FileExists(dataFileName))
                {
                    return
                        Source.ReadFile
                        (
                            string.Concat(Path, "\\", entry.Name, ".data.xml"),
                            stream =>
                            {
                                var xmlDoc = new XmlDocument();
                                xmlDoc.Load(stream);
                                return xmlDoc;
                            }
                        );
                }
                else
                {
                    return null;
                }
            }

            public void Visit(Assembly assembly)
            {
                Source.ReadFile<object>
                (
                    string.Concat(Path, "\\", assembly.Name, ".dll"),
                    stream =>
                    {
                        var buffer = new MemoryStream();
                        stream.CopyTo(buffer);
                        assembly.Image = buffer.ToArray();
                        return null;
                    }
                );
            }

            public void Visit(Folder folder)
            {
            }

            public void Visit(Link link)
            {
            }

            public void Visit(Message message)
            {
                XmlDocument data = LoadDirectoryEntryData(message);

                message.Data =
                    (data != null) ?
                        BlobUtil.WriteBlob(BlobFormat.BinXml, data.WriteTo) :
                        null;
            }

            public void Visit(Process process)
            {
            }

            public void Visit(Processor processor)
            {
                XmlDocument configuration = LoadDirectoryEntryData(processor);

                processor.Configuration =
                    (configuration != null) ?
                        BlobUtil.WriteBlob(BlobFormat.TextXml, configuration.WriteTo) :
                        null;
            }

            public void Visit(Role role)
            {
            }

            public void Visit(ServiceContainer serviceContainer)
            {
                serviceContainer.Configuration =
                        BlobUtil.WriteBlob
                        (
                            BlobFormat.TextXml,
                            LoadDirectoryEntryData(serviceContainer).WriteTo
                        );
            }

            public void Visit(Set set)
            {
            }

            public void Visit(State state)
            {
                state.Data =
                        BlobUtil.WriteBlob
                        (
                            BlobFormat.TextXml,
                            LoadDirectoryEntryData(state).WriteTo
                        );
            }

            public void Visit(Services.Data.Workflow workflow)
            {
            }

            public void Visit(DirectoryEntryInfo directoryEntryInfo)
            {
            }
        }

        #endregion

        #region DirectoryEntryTypeMapping

        private static readonly Dictionary<string, Type> DirectoryEntryTypeMapping =
            new Dictionary<string, Type>
            {
                { "dll", typeof(Assembly) },
                { "folder", typeof(Folder) },
                { "link", typeof(Link) },
                { "message", typeof(Message) },
                { "process", typeof(Process) },
                { "processor", typeof(Processor) },
                { "role", typeof(Role) },
                { "serviceContainer", typeof(ServiceContainer) },
                { "set", typeof(Set) },
                { "state", typeof(State) },
                { "workflow", typeof(Services.Data.Workflow) }
            };

        #endregion

        #region Import

        private static IDictionary<string, string> Import(IDirectoryImportSource source, IDirectory dir, string path)
        {
            var idsMapping = new ConcurrentDictionary<string, string>();
            var links = new ConcurrentBag<LinkInfo>();
            var tasks = new ConcurrentQueue<Task>();

            ImportRecursively(source, dir, path, "/", tasks, idsMapping, links);

            Task task;
            while (tasks.TryDequeue(out task))
            {
                task.Wait();
            }

            var transaction = Transaction.Current;

            links
                .AsParallel()
                .ForAll(linkInfo =>
                {
                    try
                    {
                        string mappedId = idsMapping[linkInfo.Link.Target];

                        Log.Trace
                        (
                            () =>
                                string.Format
                                (
                                    "Remapping link {0} from {1} to {2}.",
                                    string.Concat(linkInfo.Parent, "/", linkInfo.Link.Name),
                                    linkInfo.Link.Target,
                                    mappedId
                                )
                        );

                        linkInfo.Link.Target = mappedId;
                        dir.Save(linkInfo.Parent, linkInfo.Link);
                    }
                    catch (Exception ex)
                    {
                        Log.Error
                        (
                            string.Format
                            (
                                "Failed to remap link {0}.",
                                string.Concat(linkInfo.Parent, "/", linkInfo.Link.Name)
                            ),
                            ex
                        );
                    }
                });

            return idsMapping;
        }

        #endregion

        #region ImportRecursively

        private static void ImportRecursively(IDirectoryImportSource source, IDirectory dir, string path, string directoryPath, IProducerConsumerCollection<Task> tasks, IDictionary<string, string> idsMapping, IProducerConsumerCollection<LinkInfo> links)
        {
            var xmlFiles = source.EnumerateFiles(path, "*.xml");
            var reader = new DirectoryEntryReader 
            {
                Source = source,
                Path = path
            };

            foreach (string xmlFile in xmlFiles.OrderBy(name => name))
            {
                string fileName = xmlFile.Substring(0, xmlFile.Length - (".xml").Length);
                string entryTypeSuffix =
                    fileName.Substring(fileName.LastIndexOf('.') + 1);

                Type directoryEntryType;

                if (DirectoryEntryTypeMapping.TryGetValue(entryTypeSuffix, out directoryEntryType))
                {
                    string importSource = xmlFile;

                    try
                    {
                        Log.Trace(() => string.Format("Importing {0} to {1}.", importSource, directoryPath));

                        var serializer = new DataContractSerializer(directoryEntryType);

                        DirectoryEntry directoryEntry = source.ReadFile
                        (
                            xmlFile, 
                            stream =>
                                (DirectoryEntry)serializer.ReadObject(stream)
                        );

                        directoryEntry.AcceptVisitor(reader);

                        string childEntriesPath = string.Concat(path, "\\", directoryEntry.Name);
                        string srcId = directoryEntry.Id;

                        if (directoryEntry.Name == directoryEntry.Id)
                        {
                            directoryEntry.Id = null;
                            directoryEntry.Name = null;
                            directoryEntry.Id = 
                                directoryEntry.Name = 
                                dir.Save(directoryPath, directoryEntry);
                        }
                        else
                        {
                            string directoryEntryPath = string.Concat(directoryPath, "/", directoryEntry.Name);
                            DirectoryEntry existingDirectoryEntry =
                                dir.Get(directoryEntryPath);
                            if (existingDirectoryEntry != null)
                            {
                                if (existingDirectoryEntry.GetType() == directoryEntry.GetType())
                                {
                                    directoryEntry.Id = existingDirectoryEntry.Id;
                                }
                                else
                                {
                                    Log.Warning
                                    (
                                        string.Format
                                        (
                                            "Directory entry {0} exists, but it is of {1} type " +
                                            "while the entry being imported is {2}.",
                                            directoryEntryPath,
                                            existingDirectoryEntry.GetType().Name,
                                            directoryEntry.GetType().Name
                                        )
                                    );
                                    continue;
                                }
                            }
                            else
                            {
                                directoryEntry.Id = null;
                            }

                            directoryEntry.Id = dir.Save(directoryPath, directoryEntry);
                        }

                        idsMapping.Add(srcId, directoryEntry.Id);

                        var link = directoryEntry as Link;
                        if (link != null) 
                        {
                            links.TryAdd
                            (
                                new LinkInfo 
                                {
                                    Parent = directoryPath,
                                    Link = link
                                }
                            );
                        }

                        if (source.DirectoryExists(childEntriesPath))
                        {
                            var transaction = Transaction.Current;

                            tasks.TryAdd
                            (
                                Task.Factory.StartNew
                                (
                                    () =>
                                    {
                                        var prevTransaction = Transaction.Current;
                                        Transaction.Current = transaction;
                                        try
                                        {
                                            ImportRecursively
                                            (
                                                source,
                                                dir,
                                                childEntriesPath,
                                                string.Concat(directoryPath, (!directoryPath.EndsWith("/")) ? "/" : string.Empty, directoryEntry.Name),
                                                tasks,
                                                idsMapping,
                                                links
                                            );
                                        }
                                        finally
                                        {
                                            Transaction.Current = prevTransaction;
                                        }
                                    }
                                )
                            );
                        }
                    }
                    catch (Exception ex)
                    {
                        Log.Error(string.Format("Failed to import {0}.", importSource), ex);
                    }
                }
            }
        }

        #endregion

        #endregion

        #region Export

        private static string MakePath(string basePath, string parentPath)
        {
            return
                string.Concat
                (
                    basePath,
                    ((parentPath != "/") ? parentPath.Replace("/", @"\") : string.Empty)
                );
        }

        public static void ExportToFileSystem(this IDirectory dir, string path, DirectoryEntryTypes directoryEntryTypes)
        {
            Log.Trace(() => string.Format("Exporting to file system directory {0}.", path));

            Export
            (
                dir,
                (parentPath, entryName, writeAction) =>
                {
                    string fullPath = MakePath(path, parentPath);
                    string fileName = string.Concat(fullPath, @"\", entryName);

                    Log.Trace(() => string.Format("Writing {0}.", fileName));

                    System.IO.Directory.CreateDirectory(fullPath);

                    using (var output = new FileStream(fileName, FileMode.OpenOrCreate, FileAccess.Write))
                    {
                        writeAction(output);
                    }
                },
                directoryEntryTypes
            );
        }

        public static void ExportToArchive(this IDirectory dir, string path, DirectoryEntryTypes directoryEntryTypes)
        {
            Log.Trace(() => string.Format("Exporting to archive {0}.", path));

            using (var output = new FileStream(path, FileMode.OpenOrCreate, FileAccess.Write))
            {
                dir.ExportToArchive(output, directoryEntryTypes);
            }
        }

        public static void ExportToArchive(this IDirectory dir, Stream output, DirectoryEntryTypes directoryEntryTypes)
        {
            Log.Trace("Exporting to stream archive.");

            using(var zipFile = new ZipFile())
            {
                Export
                (
                    dir,
                    (parentPath, entryName, writeAction) =>
                    {
                        var stream = new MemoryStream();
                        writeAction(stream);
                        stream.Position = 0;

                        parentPath = parentPath.TrimStart('/');

                        string fileName = 
                            (parentPath.Length > 0) ?
                            string.Concat(parentPath, @"/", entryName) :
                            entryName;

                        Log.Trace(() => string.Format("Writing {0}.", fileName));

                        lock (zipFile)
                        {
                            if (parentPath.Length > 0)
                            {
                                string dirName = parentPath + "/";

                                if (!zipFile.ContainsEntry(dirName))
                                {
                                    zipFile.AddDirectoryByName(dirName);
                                }
                            }

                            zipFile.AddEntry(fileName, stream);
                        }
                    },
                    directoryEntryTypes
                );
                    
                zipFile.Save(output);
            }
        }

        private enum DirectoryEntryExportRequestAction
        {
            Begin,
            Export,
            End
        }

        private struct DirectoryEntryExportRequest
        {
            public DirectoryEntryExportRequestAction Action;
            public string ParentPath;
            public DirectoryEntry Entry;
            public long ChildIndex;
        }

        #region DirectoryEntryNamer

        private class DirectoryEntryNamer : IDirectoryEntryVisitor
        {
            public long ChildIndex;
            public string Name;

            public void Visit(Assembly assembly)
            {
                Name = string.Concat(assembly.Name, ".dll");
            }

            public void Visit(Folder folder)
            {
                Name = string.Concat(folder.Name, ".folder.xml");
            }

            public void Visit(Link link)
            {
                Name = string.Concat(link.Name, ".link.xml");
            }

            public void Visit(Message message)
            {
                Name = string.Concat(ChildIndex.ToString("D20"), "_", message.Name, ".message");
            }

            public void Visit(Process process)
            {
                Name = string.Concat(process.Name, ".process.xml");
            }

            public void Visit(Processor processor)
            {
                Name = string.Concat(processor.Name, ".processor");
            }

            public void Visit(Role role)
            {
                Name = string.Concat(role.Name, ".role.xml");
            }

            public void Visit(ServiceContainer serviceContainer)
            {
                Name = string.Concat(serviceContainer.Name, ".serviceContainer");
            }

            public void Visit(Set set)
            {
                Name = string.Concat(set.Name, ".set.xml");
            }

            public void Visit(State state)
            {
                Name = string.Concat(ChildIndex.ToString("D20"), "_", state.Name, ".state");
            }

            public void Visit(Services.Data.Workflow workflow)
            {
                Name = string.Concat(workflow.Name, ".workflow.xml");
            }

            public void Visit(DirectoryEntryInfo directoryEntryInfo)
            {
            }
        }

        #endregion

        #region DirectoryEntryWriter

        private class DirectoryEntryWriter : IDirectoryEntryVisitor
        {
            public readonly DirectoryEntryNamer Namer = new DirectoryEntryNamer();

            public string ParentPath;
            public Action<string, string, Action<Stream>> WriteOutputStream;

            public void Visit(Assembly assembly)
            {
                assembly.AcceptVisitor(Namer);

                WriteOutputStream(ParentPath, Namer.Name, output =>
                {
                    output.Write(assembly.Image, 0, assembly.Image.Length);

                    output.Flush();
                });

                WriteOutputStream(ParentPath, Namer.Name + ".xml", output =>
                {
                    assembly.Image = null;

                    assembly.WriteAsReadableXml(output);
                    output.Flush();
                });
            }

            public void Visit(Folder folder)
            {
                folder.AcceptVisitor(Namer);

                WriteOutputStream(ParentPath, Namer.Name, output =>
                {
                    folder.WriteAsReadableXml(output);

                    output.Flush();
                });
            }

            public void Visit(Link link)
            {
                link.AcceptVisitor(Namer);

                WriteOutputStream(ParentPath, Namer.Name, output =>
                {
                    link.WriteAsReadableXml(output);

                    output.Flush();
                });
            }

            public void Visit(Message message)
            {
                message.AcceptVisitor(Namer);

                if (message.Data != null)
                {
                    WriteOutputStream(ParentPath, message.Name + ".data.xml", output =>
                    {
                        message.Data.WriteAsReadableXml(output);

                        output.Flush();
                    });
                }

                WriteOutputStream(ParentPath, Namer.Name + ".xml", output =>
                {
                    message.Data = null;

                    message.WriteAsReadableXml(output);
                    output.Flush();
                });
            }

            public void Visit(Process process)
            {
                process.AcceptVisitor(Namer);

                WriteOutputStream(ParentPath, Namer.Name, output =>
                {
                    process.WriteAsReadableXml(output);

                    output.Flush();
                });
            }

            public void Visit(Processor processor)
            {
                processor.AcceptVisitor(Namer);

                if (processor.Configuration != null)
                {
                    WriteOutputStream(ParentPath, processor.Name + ".data.xml", output =>
                    {
                        processor.Configuration.WriteAsReadableXml(output);

                        output.Flush();
                    });
                }

                WriteOutputStream(ParentPath, Namer.Name + ".xml", output =>
                {
                    processor.Configuration = null;

                    processor.WriteAsReadableXml(output);
                    output.Flush();
                });
            }

            public void Visit(Role role)
            {
                role.AcceptVisitor(Namer);

                WriteOutputStream(ParentPath, Namer.Name, output =>
                {
                    role.WriteAsReadableXml(output);

                    output.Flush();
                });
            }

            public void Visit(ServiceContainer serviceContainer)
            {
                serviceContainer.AcceptVisitor(Namer);

                WriteOutputStream(ParentPath, serviceContainer.Name + ".data.xml", output =>
                {
                    serviceContainer.Configuration.WriteAsReadableXml(output);

                    output.Flush();
                });

                WriteOutputStream(ParentPath, Namer.Name + ".xml", output =>
                {
                    serviceContainer.Configuration = null;

                    serviceContainer.WriteAsReadableXml(output);
                    output.Flush();
                });
            }

            public void Visit(Set set)
            {
                set.AcceptVisitor(Namer);

                WriteOutputStream(ParentPath, Namer.Name, output =>
                {
                    set.WriteAsReadableXml(output);

                    output.Flush();
                });
            }

            public void Visit(State state)
            {
                state.AcceptVisitor(Namer);

                WriteOutputStream(ParentPath, state.Name + ".data.xml", output =>
                {
                    state.Data.WriteAsReadableXml(output);

                    output.Flush();
                });

                WriteOutputStream(ParentPath, Namer.Name + ".xml", output =>
                {
                    state.Data = null;

                    state.WriteAsReadableXml(output);
                    output.Flush();
                });
            }

            public void Visit(Services.Data.Workflow workflow)
            {
                workflow.AcceptVisitor(Namer);

                WriteOutputStream(ParentPath, Namer.Name, output =>
                {
                    workflow.WriteAsReadableXml(output);

                    output.Flush();
                });
            }

            public void Visit(DirectoryEntryInfo directoryEntryInfo)
            {
            }
        }

        #endregion

        #region Export

        private static void Export
        (
            IDirectory dir, 
            Action<string, string, Action<Stream>> writeOutputStream,
            DirectoryEntryTypes directoryEntryTypes
        )
        {
            var exportChannel = new BlockingCollection<DirectoryEntryExportRequest>();

            LoadDirectoryEntriesRecursively(dir, exportChannel, "/", directoryEntryTypes);

            var writer = new DirectoryEntryWriter 
            {
                WriteOutputStream = writeOutputStream
            };
            long level = 0;

            do
            {
                DirectoryEntryExportRequest req = exportChannel.Take();

                switch (req.Action)
                {
                    case DirectoryEntryExportRequestAction.Begin:
                        ++level;
                        break;
                    case DirectoryEntryExportRequestAction.End:
                        --level;
                        break;
                    case DirectoryEntryExportRequestAction.Export:
                        {
                            writer.Namer.ChildIndex = req.ChildIndex;
                            writer.ParentPath = req.ParentPath;

                            req.Entry.AcceptVisitor(writer);
                        }
                        break;
                    default:
                        throw new ArgumentOutOfRangeException();
                }
            } while (level > 0);
        }

        #endregion

        #region LoadDirectoryEntriesRecursively

        private static void LoadDirectoryEntriesRecursively
        (
            IDirectory dir, 
            BlockingCollection<DirectoryEntryExportRequest> exportChannel,
            string parent,
            DirectoryEntryTypes directoryEntryTypes
        )
        {
            const int batchSize = 100;

            exportChannel.Add
            (
                new DirectoryEntryExportRequest
                {
                    Action = DirectoryEntryExportRequestAction.Begin
                }
            );

            var transaction = Transaction.Current;

            Task.Factory.StartNew(() =>
            {
                long childIndex = 0;
                long offset = 0;
                long limit = batchSize;

                string basePath = 
                    (parent[parent.Length - 1] != '/') ? parent + "/" : parent;

                Log.Trace(() => string.Format("Loading children of {0}.", parent));

                var prevTransaction = Transaction.Current;
                Transaction.Current = transaction;
                try
                {
                    IList<DirectoryEntry> children;

                    do
                    {
                        children =
                            dir.GetChildren
                            (
                                parent,
                                directoryEntryTypes | DirectoryEntryTypes.Folder,
                                SortProperty.Default,
                                SortOrder.Asc,
                                offset,
                                limit
                            );

                        foreach (DirectoryEntry child in children)
                        {
                            string childName = child.Name;
                            Log.Trace(() => string.Format("Child {0} of {1} loaded.", childName, parent));

                            exportChannel.Add
                            (
                                new DirectoryEntryExportRequest
                                {
                                    Action = DirectoryEntryExportRequestAction.Export,
                                    ParentPath = parent,
                                    Entry = child,
                                    ChildIndex = childIndex++
                                }
                            );

                            LoadDirectoryEntriesRecursively(dir, exportChannel, basePath + child.Name, directoryEntryTypes);
                        }

                        offset = limit;
                        limit += batchSize;
                    } while (children.Count > 0);
                }
                catch (Exception ex)
                {
                    Log.Error(string.Format("Failed to load children of {0}.", parent), ex);
                }
                finally
                {
                    Transaction.Current = prevTransaction;
                }

                exportChannel.Add
                (
                    new DirectoryEntryExportRequest
                    {
                        Action = DirectoryEntryExportRequestAction.End
                    }
                );
            });
        }

        #endregion

        #endregion
    }
}
